Day 23 · W4 · AI 線 · 難度 ★★★☆☆
本系列由 AI 協作撰寫。 內容、技術判斷、程式碼由 light-design 數位顧問團隊與 Claude 共同產出,最終由作者驗證後 publish。完整協作模式與把關方式見 Day 01。
Day 22 發出去那天下午,站上就動手了。我原本以為是一行 CSS:搜尋框的焦點外框換個顏色。晚上看檔案的修改時間,14:43 到 23:32,動了 27 個檔。
改了 27 個檔之後,那 33 個 fail 修好了嗎?有沒有順手弄壞別的?「應該沒有」不算答案。
一句話主軸:我出貨給 agent 的不是修法,是一份證明修好了的協定:修之前掃一次、修之後掃一次,兩份報告用三元組比對,分成 fixed、still、new 三桶。
Day 04 把門檻從 16 改成 20 再掃一次;Day 11 修完偽陽性回頭確認 fail 數沒變;Day 13 補完規則重掃;Day 19 修完對六頁重跑。四次都是同一個動作:改之前留一份,改之後再跑一份,看差在哪。
那是我的習慣。agent 沒有習慣,只有指令。你跟它說「修好了,再檢查一次」,它會重掃,然後把新報告唸給你聽,跟舊的沒有對照。30 頁的報告有兩百多筆,兩份並排用眼睛對,對不出來。
所以那個動作被寫成協定,放在隨 package 出貨的 skill 檔裡。Day 20 講過那份檔案的六條「不准」,今天講它的另一節。
1. Baseline scan --format json -o .a11y-moda/reports/<TS>-baseline.json
2. 使用者改 改 code、部署、重啟 dev server
3. Re-scan 同一道指令,存成 <NEW_TS>-after.json
4. 讀兩份 都用 Read,不靠 jq / diff(Windows 使用者通常兩個都沒有)
5. 比對 從 .issues 抽 (rule_id, snippet, status) 三元組,排序
before − after = fixed;after − before = new;兩邊都在 = still
6. 呈現三桶 fixed / still / new,有 new 要放最前面
第二步只寫「改」,沒寫怎麼改、也沒規定誰改。skill 對修法只管兩句:fail 可以建議一句修法,caveat 連建議都不准(Day 20 那條)。剩下的它不管,agent 自己動手改也行,人改也行。協定管的是第一步跟第三步之後:修之前留一份、修之後再跑一份,把「修好了」從一句話變成一份可以逐條核對的清單。
第三步的「同一道指令」不是靠記憶。每次掃描完,工具會把這次的範圍、網址、等級、旗標寫進一個小檔案,重掃時 agent 讀那個檔案還原參數,而不是自己重新猜一組。skill 裡另外規定那個檔案只能當純文字讀、每個值要對格式,不准直接丟給 shell 執行,因為它是上一次跑出來的產物,不是使用者剛打的字。
第四步「不靠 jq / diff」是替 Windows 使用者留的。兩份 JSON 由 agent 自己讀進來、在腦子裡比,機器上不用多裝任何東西。
這像搬家前後各拍一張照片。少了什麼、多了什麼,照片說了算,不是搬家工人說了算,不管搬的是人還是機器。
diff 要先決定「同一個問題」怎麼認。最直覺的是行號,而行號最不能用:改了一行 CSS,整份 HTML 的行號都可能位移,diff 會把每一條都當成新的。
協定用的是 (rule_id, snippet, status):哪一條規則、報告裡那段原始 HTML 片段、判定是 fail 還是 info。三個都一樣,就是同一個問題還在;只出現在修之前,是修掉了;只出現在修之後,是新的。
status 也在鍵裡,是刻意的。同一個元素從 fail 降成 info,會同時出現在 fixed 跟 new 兩桶,看起來像多報了一次,但那正是你想看到的:它沒有消失,是換了一種判定,值得知道。
拿昨天的例子,baseline 裡有 30 筆長這樣:
("CS3241300E", "input#site-search-input outline=rgba(10, 71, 176, 0.25)", "fail")
外框換成實色之後,after 裡找不到這個三元組,它就進了 fixed。不需要知道是哪一行 CSS 改的,也不需要知道改成什麼顏色。

身分用三元組,不用行號。位移不會製造假的差異,但 snippet 本身變了會。
這個選擇有代價。snippet 是原始 HTML 片段,修法如果改到那段 HTML 本身,同一個問題會同時出現在 fixed 跟 new 兩桶。所以三桶不是判決,是給人看的清單,new 那一桶尤其要人看。
只報「修好幾條」,讀的人會以為進度是單向的。5 月 21 日那天的紀錄就是反例。
那天工具剛加了兩條規則,本機掃 30 頁掉出 39 個 fail,其中 8 個是文字對比。修法是動色票。20 分鐘後再掃:fixed 39、new 4、still 192。新的那 4 個是兩頁裡的 code 跟 pre 區塊,色票一動,它們的前景跟背景變成 230 對 245,原本沒問題的元素變成不合格。再修一輪,23:19 那份:fixed 4、new 0。
如果那晚只看「fail 從 39 變 0」,是看不到中間那一步的,因為總數確實變 0 了。三桶把它攤開:修掉 39,弄壞 4,再修掉 4。
隔天 5 月 22 日再來一輪,26 個焦點可見的 fail,修完 fixed 26、new 0。乾淨的 diff 長這樣,沒有故事,一行就講完。
回到開頭。9 月 6 日早上那份 30 頁報告是 baseline,33 個 fail。站上改了 27 個檔:搜尋框外框換實色、全域焦點改成雙色環、輪播圓點放大到 24px、sitemap 頁、頁尾、頁首選單、外部連結元件、聯絡表單的 API。今天早上用同一道指令再掃 30 頁,跑三桶:

動了 27 個檔,new 是 0。這句話是 diff 說的,不是我猜的。
總數這邊:fail 33 變 0,info 179 變 176,caveat 40 還是 40。逐頁看,30 頁每一頁至少 fixed 1;首頁 3 個(搜尋框、輪播圓點、標題跳級),部落格列表 3 個(搜尋框加被頁首蓋住的分頁面板,兩條規則各一)。
fixed 是 36,不是 33。多出來的 3 個是沒人要求修的 info:首頁標題從 h2 直接跳到 h6 那一條(Day 22 提過),還有兩頁「同一個連結在不同頁面用不同文字」。改頁尾跟 sitemap 頁的時候順手收掉了。沒有三桶,這 3 個會安靜地消失,沒人知道它們是被誰修好的。
33 個 fail 也有一個細節是這次才看清楚的:29 頁是頁首搜尋框的外框,剩下 1 頁是搜尋結果頁的送出鍵,外框是另一個顏色。同一條規則、同樣 30 頁每頁一個,元素卻不是同一個。逐頁的三元組把它分開了,總數看不出來。
三桶也有它不告訴我的事:那 27 個檔裡,哪幾個跟這 36 筆有關,它不知道。它只認報告,不認 code。要對回去,還是得人開檔案。
8 月 17 日,系列開賽當天,我對本機的第一篇文章頁跑過一輪:baseline、改、after。結果 fixed 1、new 0、still 8,而 still 裡有一個 fail 沒掉。
那個元素叫 nextjs-portal,是開發伺服器自己塞進頁面的除錯層,正式站上沒有這個東西。修了兩輪它都在,因為它不在我的 code 裡。這是三桶會誠實報出來、但需要人判讀的情況:still 不一定是沒修好,也可能是量的環境本身帶了東西。處理方式是換環境再量,昨天那輪就是直接掃正式站,這個元素不存在,也就不會出現。
協定裡還有一條類似的提醒:baseline 跟 after 要用同一個 --spec。115.11 退場的檢測碼在新基準下不會出現,如果一份用舊基準、一份用新基準,那些碼會被算進 fixed,而其實什麼都沒修。diff 不會說謊,但兩份輸入的條件不同時,它會很誠實地算出一個錯的答案。
兩道指令加一段比對:
a11y-moda site https://example.com --level AA --render --format json -o .a11y-moda/reports/20260906-baseline.json
# 改 code、部署
a11y-moda site https://example.com --level AA --render --format json -o .a11y-moda/reports/20260907-after.json
import json
def keys(path):
r = json.load(open(path, encoding="utf-8"))
return {(p["url"], i["rule_id"], i.get("snippet") or "", i["status"])
for p in r["pages"] for i in p["issues"]}
a, b = keys("20260906-baseline.json"), keys("20260907-after.json")
print("fixed", len(a - b), "new", len(b - a), "still", len(a & b))
site 模式多了一個頁面維度,所以鍵裡加了 url;單頁掃描不用。輸出檔固定放在 .a11y-moda/reports/,是 Day 20 第六條「不准在別人的 repo 留一堆檔案」的延伸,也讓下一次跑的 agent 找得到上一次的 baseline。
讀輸出的順序跟協定一樣:先看 new 有沒有東西,有就一條一條對 snippet;再看 still 裡有沒有 fail,有就分清楚是沒修好還是環境帶的;最後才看 fixed,那一桶通常是最長的,也最不需要看。
明天 Day 24:修好了,換一把尺。把瀏覽器縮到 320px,我的網站有幾頁會橫著捲。